import { TranslatableError } from '@/common/error/translatable-error';
import { createAppUrn } from '@/common/helpers/app-helpers';
import { pLimit } from '@/common/helpers/file-helpers';
import { LoggerService } from '@/core/logger/logger.service';
import { Injectable } from '@nestjs/common';
import type { AppUrn } from '@runtipi/common/types';
import { CURRENT_SCHEMA_VERSION, parseComposeJson } from '@runtipi/common/schemas';
import { MarketplaceService } from '../marketplace/marketplace.service';
import { AppFilesManager } from './app-files-manager';
import { AppsRepository } from './apps.repository';

type AppList = Awaited<ReturnType<AppsRepository['getApps']>>;

@Injectable()
export class AppsService {
  constructor(
    private readonly appsRepository: AppsRepository,
    private readonly appFilesManager: AppFilesManager,
    private readonly logger: LoggerService,
    private readonly marketplaceService: MarketplaceService,
  ) {}

  private async populateAppInfo(apps: AppList) {
    const limit = pLimit(10);

    const populatedApps = await Promise.all(
      apps.map(async (app) => {
        return limit(async () => {
          const appUrn = createAppUrn(app.appName, app.appStoreSlug);
          const appInfo = await this.appFilesManager.getInstalledAppInfo(appUrn);

          const updateInfo = await this.marketplaceService.getAppUpdateInfo(appUrn).catch((_) => {
            return { latestVersion: 0, latestDockerVersion: '0.0.0' };
          });

          if (!appInfo) {
            this.logger.debug(`App ${app.id} not found in app files`);
            return null;
          }

          let composeSchemaVersion: number | undefined;
          try {
            const compose = await this.appFilesManager.getDockerComposeJson(appUrn);
            if (compose.content) {
              const parsed = parseComposeJson(compose.content) as unknown as { _schemaVersion: number };
              composeSchemaVersion = parsed._schemaVersion;
            }
          } catch (error) {
            this.logger.debug(`Could not parse compose schema version for ${appUrn}:`, error);
          }

          const localSubdomain = app.localSubdomain || appUrn.split(':').join('-');
          return {
            app,
            info: appInfo,
            metadata: { ...updateInfo, localSubdomain, composeSchemaVersion: composeSchemaVersion ?? CURRENT_SCHEMA_VERSION },
          };
        });
      }),
    );

    return populatedApps.filter((app) => app !== null);
  }

  /**
   * Get the installed apps
   */
  public async getInstalledApps() {
    const apps = await this.appsRepository.getApps();

    return this.populateAppInfo(apps);
  }

  public async getGuestDashboardApps() {
    this.logger.debug('Getting guest dashboard apps');
    const apps = await this.appsRepository.getGuestDashboardApps();
    this.logger.debug(`Got ${apps.length} guest dashboard apps`);

    return this.populateAppInfo(apps);
  }

  public async getApp(appUrn: AppUrn) {
    const app = await this.appsRepository.getAppByUrn(appUrn);
    const updateInfo = await this.marketplaceService.getAppUpdateInfo(appUrn).catch((_) => {
      return { latestVersion: 0, latestDockerVersion: '0.0.0' };
    });

    let info = await this.appFilesManager.getInstalledAppInfo(appUrn);

    const userCompose = await this.appFilesManager.getUserComposeFile(appUrn);
    const userEnv = await this.appFilesManager.getUserEnv(appUrn);
    const hasCustomConfig = Boolean(userCompose.content) || Boolean(userEnv.content);

    if (!info) {
      info = (await this.marketplaceService.getAppInfoFromAppStore(appUrn)) ?? null;
    }

    if (!info) {
      throw new TranslatableError('APP_ERROR_APP_NOT_FOUND', {}, 404);
    }

    const localSubdomain = app?.localSubdomain || appUrn.split(':').join('-');

    let composeSchemaVersion: number | undefined;
    try {
      const compose = await this.appFilesManager.getDockerComposeJson(appUrn);
      if (compose.content) {
        const parsed = parseComposeJson(compose.content) as unknown as { _schemaVersion: number };
        composeSchemaVersion = parsed._schemaVersion;
      }
    } catch (error) {
      this.logger.debug(`Could not parse compose schema version for ${appUrn}:`, error);
    }

    const metadata = {
      hasCustomConfig,
      localSubdomain,
      composeSchemaVersion: composeSchemaVersion ?? CURRENT_SCHEMA_VERSION,
      ...updateInfo,
    };

    return { app: app ?? null, info, metadata };
  }

  public async getRandomPort(tries = 3): Promise<number> {
    if (tries <= 0) {
      throw new Error('Failed to get random port after 3 tries');
    }

    const port = Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
    const apps = await this.appsRepository.getAppsByPort(port);

    if (apps.length === 0) {
      return port;
    }

    return this.getRandomPort(tries - 1);
  }

  public async getAppComposeDiff(appUrn: AppUrn) {
    const app = await this.appsRepository.getAppByUrn(appUrn);
    if (!app) {
      throw new TranslatableError('APP_ERROR_APP_NOT_FOUND', {}, 404);
    }

    const [currentCompose, storeCompose] = await Promise.all([
      this.appFilesManager.getDockerComposeJson(appUrn),
      this.marketplaceService.getDockerComposeJson(appUrn),
    ]);

    return {
      current: currentCompose.content ? JSON.stringify(currentCompose.content, null, 2) : null,
      new: storeCompose.content ? JSON.stringify(storeCompose.content, null, 2) : null,
    };
  }

  public async getAppConfigDiff(appUrn: AppUrn) {
    const app = await this.appsRepository.getAppByUrn(appUrn);
    if (!app) {
      throw new TranslatableError('APP_ERROR_APP_NOT_FOUND', {}, 404);
    }

    const [currentConfig, storeConfig] = await Promise.all([
      this.appFilesManager.getConfigJson(appUrn),
      this.marketplaceService.getConfigJson(appUrn),
    ]);

    return {
      current: currentConfig.content ? JSON.stringify(currentConfig.content, null, 2) : null,
      new: storeConfig.content ? JSON.stringify(storeConfig.content, null, 2) : null,
    };
  }

  public async ignoreAppVersion(appUrn: AppUrn) {
    const app = await this.appsRepository.getAppByUrn(appUrn);

    if (!app) {
      throw new TranslatableError('APP_ERROR_APP_NOT_FOUND', {}, 404);
    }

    const { latestVersion } = await this.marketplaceService.getAppUpdateInfo(appUrn);

    await this.appsRepository.updateAppById(app.id, { ignoredVersion: latestVersion });

    this.logger.info(`Ignored version ${latestVersion} for app ${appUrn}`);
  }

  public async unignoreAppVersion(appUrn: AppUrn) {
    const app = await this.appsRepository.getAppByUrn(appUrn);

    if (!app) {
      throw new TranslatableError('APP_ERROR_APP_NOT_FOUND', {}, 404);
    }

    await this.appsRepository.updateAppById(app.id, { ignoredVersion: null });

    this.logger.info(`Unignored version for app ${appUrn}`);
  }
}
